/*-----------------------------------------------------------------------
 *  MediaFrame is an Open Source streaming media platform in Java 
 *  which provides a fast, easy to implement and extremely small applet 
 *  that enables to view your audio/video content without having 
 *  to rely on external player applications or bulky plug-ins.
 *
 *-----------------------------------------------------------------------
 *
 *  We changed a lot of code and added a lot of functionality.
 *  This includes, but not limited to, the following changes:
 *  1. The project was renamed to MediaFrame;
 *  2. The connection speed detection procedure was added;
 *  3. The JavaScript API functions were added;
 *  4. The pre and post image support functionality was added;
 *  5. The ability to save movie into the local disk was added;
 *  6. The inner buffer for a movie file was added;
 *  7. The click-through functionality was added;    
 *  8. The .zip files support was added;    
 *  9. The realtime feedback agent functionality was added.    
 *  For the full list of the current functionality please visit 
 *  the following web page: http://mediaframe.org/
 *    
 *  06 Jul 2002 - 19 Dec 2004 Konstantin Belous, Oleg Lebedev
 *
 *-----------------------------------------------------------------------
 * 1/12/99		Initial version.	mdm@techie.com
/*-----------------------------------------------------------------------
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *----------------------------------------------------------------------
 */


package mediaframe.mpeg1.audio.decoder;

import java.io.IOException;
import java.io.EOFException;
import mediaframe.mpeg1.io_tool;


/**
 * The <code>Decoder</code> class encapsulates the details of
 * decoding an MPEG audio frame. 
 * 
 * @author	MDM	
 * @version 0.0.7 12/12/99
 * @since	0.0.5
 */

public class Decoder implements DecoderErrors
{
	static private final Params DEFAULT_PARAMS = new Params();
	
	/**
	 * The Obuffer instance that will receive the decoded
	 * PCM samples.
	 */
	private Obuffer			output;
		
	/**
	 * Synthesis filter for the left channel.
	 */
	private SynthesisFilter			filter1;
	
	/**
	 * Sythesis filter for the right channel.
	 */
	private SynthesisFilter			filter2;	
			
	/**
	 * The decoder used to decode layer III frames.
	 */
//  private LayerIIIDecoder			l3decoder;
//	private LayerIIDecoder			l2decoder;
//	private LayerIDecoder			l1decoder;
	private FrameDecoder[] decoders = new FrameDecoder[64];
	
	private int						outputFrequency;
	private int						outputChannels;
	
	private Equalizer				equalizer = new Equalizer();
	
	private Params					params;
	
	private boolean					initialized;
		
	
	/**
	 * Creates a new <code>Decoder</code> instance with default 
	 * parameters.
	 */
	
	public Decoder() {
		this(null);
	}

	/**
	 * Creates a new <code>Decoder</code> instance with customizable 
	 * parameters.
	 * 
	 * @param params0	The <code>Params</code> instance that describes
	 *					the customizable aspects of the decoder.  
	 */
	public Decoder(Params params0) {
		if (params0 == null)
			params0 = DEFAULT_PARAMS;
	
		params = params0;
		
		Equalizer eq = params.getInitialEqualizerSettings();
		if (eq!=null) {
			equalizer.setFrom(eq);
		}
	}
	
	static public Params getDefaultParams() {
		return (Params)DEFAULT_PARAMS.clone();
	}
	
	public void setEqualizer(Equalizer eq) {
		if (eq==null)
			eq = Equalizer.PASS_THRU_EQ;
		
		equalizer.setFrom(eq);
		
		float[] factors = equalizer.getBandFactors();
		if (filter1!=null)
			filter1.setEQ(factors);
		
		if (filter2!=null)
			filter2.setEQ(factors);			
	}
	
	/**
	 * Decodes one frame from an MPEG audio bitstream.
	 * 
	 * @param header		The header describing the frame to decode.
	 * @param stream		The bistream that provides the bits for the body of the frame. 
	 * 
	 * @return An Obuffer containing the decoded samples.
	 */
	public Obuffer decodeFrame(Header header, io_tool stream) throws InterruptedException, DecoderException, EOFException {
//System.out.println("--> DECODE Frame");
		if (!initialized) {
			initialize(header);
		}
		
		output.clear_buffer();
		
		FrameDecoder decoder = retrieveDecoder(header, stream);
		

		// BYOHack:
		// System.out.println("layer: " + layer);
		// System.out.println("num subbands:" + header.number_of_subbands());
		decoder.decodeFrame();
				
		output.write_buffer(1);

//System.out.println("DECODE Frame -->");
		
		return output;	
	}
	
	/**
	 * Changes the output buffer. This will take effect the next time
	 * decodeFrame() is called. 
	 */
	public void setOutputBuffer(Obuffer out)
	{
		output = out;
	}
	
	/**
	 * Retrieves the sample frequency of the PCM samples output
	 * by this decoder. This typically corresponds to the sample
	 * rate encoded in the MPEG audio stream.
	 * 
	 * @return the sample rate (in Hz) of the samples written to the
	 *		output buffer when decoding. 
	 */
	public int getOutputFrequency()
	{
		return outputFrequency;
	}
	
	/**
	 * Retrieves the number of channels of PCM samples output by
	 * this decoder. This usually corresponds to the number of
	 * channels in the MPEG audio stream, although it may differ.
	 * 
	 * @return The number of output channels in the decoded samples: 1 
	 *		for mono, or 2 for stereo.
	 *		
	 */
	public int getOutputChannels()
	{
		return outputChannels;	
	}
	
	/**
	 * Retrieves the maximum number of samples that will be written to
	 * the output buffer when one frame is decoded. This can be used to
	 * help calculate the size of other buffers whose size is based upon 
	 * the number of samples written to the output buffer. NB: this is
	 * an upper bound and fewer samples may actually be written, depending
	 * upon the sample rate and number of channels.
	 * 
	 * @return The maximum number of samples that are written to the 
	 *		output buffer when decoding a single frame of MPEG audio.
	 */
	public int getOutputBlockSize()
	{
		return output.OBUFFERSIZE;
	}
	
	
	protected DecoderException newDecoderException(int errorcode)
	{
		return new DecoderException(errorcode, null);
	}
	
	protected DecoderException newDecoderException(int errorcode, Throwable throwable)
	{
		return new DecoderException(errorcode, throwable);
	}
	
	protected FrameDecoder retrieveDecoder(Header header, io_tool stream)
		throws DecoderException
	{
		int decoderIndex = (header.layer() << 4) + (header.mode() << 2) + (header.mode_extension()); 
		FrameDecoder decoder = decoders[decoderIndex];
//		FrameDecoder decoder = null;
		
		// REVIEW: allow channel output selection type
		// (LEFT, RIGHT, BOTH, DOWNMIX)
		switch (header.layer()) {
		case 3:
			// Layer III support temorarily removed (saves space in jar)
			throw newDecoderException(UNSUPPORTED_LAYER, null);
			/*
			if (l3decoder == null) {
				l3decoder = new LayerIIIDecoder(stream, 
												header, filter1, filter2, 
												output, OutputChannels.BOTH_CHANNELS);
			}
			decoder = l3decoder;
			break;
			*/
		case 2:
			if (decoder == null) {
				
				decoder = new LayerIIDecoder();
				((LayerIIDecoder)decoder).create(stream, 
							   header, filter1, filter2, 
							   output, OutputChannels.BOTH_CHANNELS);				
			}
			break;
		case 1:
			if (decoder == null) {
				decoder = new LayerIDecoder();
				((LayerIDecoder)decoder).create(stream, 
								 header, filter1, filter2, 
								 output, OutputChannels.BOTH_CHANNELS);				
			}
			break;
		}
						
		if (decoder == null) {
			throw newDecoderException(UNSUPPORTED_LAYER, null);
		} else {
			decoders[decoderIndex] = decoder;
		}
		
		return decoder;
	}
	
	private void initialize(Header header)
		throws DecoderException
	{
		
		// REVIEW: allow customizable scale factor
		float scalefactor = 32700.0f;
		
		int mode = header.mode();
		int layer = header.layer();
		int channels = mode==Header.SINGLE_CHANNEL ? 1 : 2;

		// set up output buffer if not set up by client.
		if (output==null)
			output = new Obuffer(header.frequency(), channels);
		
		float[] factors = equalizer.getBandFactors();
		filter1 = new SynthesisFilter(0, scalefactor, factors, header.sample_frequency(),
									  params.getDownconvert());
   		
		// REVIEW: allow mono output for stereo
		if (channels==2) 
			filter2 = new SynthesisFilter(1, scalefactor, factors, header.sample_frequency(),
										  params.getDownconvert());

		outputChannels = channels;
		outputFrequency = header.frequency();
		
		initialized = true;
	}

	public Params getParams() {
		return params;
	}
	
	/**
	 * The <code>Params</code> class presents the customizable
	 * aspects of the decoder. 
	 * <p>
	 * Instances of this class are not thread safe. 
	 */
	public static class Params implements Cloneable
	{
		private OutputChannels	outputChannels = OutputChannels.BOTH;
		private Equalizer		equalizer = new Equalizer();
		private boolean			downConvert = false;

		public Params() { }
		
		public Object clone() {
			try {
				return super.clone();
			}
			catch (CloneNotSupportedException ex) {
				throw new InternalError(this + ": " + ex);
			}
		}
				
		public void setOutputChannels(OutputChannels out) {
			if (out == null)
				throw new NullPointerException("out");
			outputChannels = out;
		}

		public OutputChannels getOutputChannels() {
			return outputChannels;
		}

		// Output converts to 8khz ulaw
		public void setDownconvert() {
			downConvert = true;
		}

		public boolean getDownconvert() {
			return downConvert; 
		}
		
		/**
		 * Retrieves the equalizer settings that the decoder's equalizer
		 * will be initialized from.
		 * <p>
		 * The <code>Equalizer</code> instance returned 
		 * cannot be changed in real time to affect the 
		 * decoder output as it is used only to initialize the decoders
		 * EQ settings. To affect the decoder's output in realtime,
		 * use the Equalizer returned from the getEqualizer() method on
		 * the decoder. 
		 * 
		 * @return	The <code>Equalizer</code> used to initialize the
		 *			EQ settings of the decoder. 
		 */
		public Equalizer getInitialEqualizerSettings() {
			return equalizer;	
		}
				
	};
}
